#!/usr/bin/env python
#
# vc: The common Version Control front-end
# Copyright (C) 2011 James Newton
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import os, sys, re, types, commands, stat
from optparse import OptionParser
from ConfigParser import ConfigParser, NoOptionError


### Commands
(CMD_STATUS, CMD_VSTATUS, CMD_CLONE, CMD_COMMIT, CMD_ADD, CMD_REMOVE, CMD_LOG,
 CMD_REVERT, CMD_UPDATE, CMD_PULL, CMD_PUSH, CMD_SYNC, CMD_PEND, CMD_URL,
 CMD_BACKUP, CMD_RESTORE, CMD_DIFF, CMD_TOP, CMD_LIST, CMD_FIND,
 CMD_PROJ, CMD_HELP) = range(22)

### Command def keywords
(CMD, MULTI, FULL, IGNORE) = range(4)
(ASK_ASK, ASK_SAY, ASK_NO) = range(3)

CMD_NAMES = {
    CMD_STATUS : "status",
    CMD_VSTATUS : "vstatus",
    CMD_CLONE : "clone",
    CMD_COMMIT : "commit",
    CMD_ADD : "add",
    CMD_REMOVE : "remove",
    CMD_LOG : "log",
    CMD_REVERT : "revert",
    CMD_UPDATE : "update",
    CMD_PULL : "pull",
    CMD_PUSH : "push",
    CMD_SYNC : "sync",
    CMD_PEND : "pend",
    CMD_URL : "url",
    CMD_BACKUP : "backup",
    CMD_RESTORE : "restore",
    CMD_DIFF : "diff",
    CMD_TOP : "top",
    CMD_LIST : "list",
    CMD_FIND : "find",
    CMD_PROJ : "proj",
    CMD_HELP : "help",
    }

URL_CMDS = [CMD_CLONE, CMD_LIST]

# Yeah a trie would be better, but its really not required and I dont feel like
# making one, so there.
CMDS = {
    "status" : CMD_STATUS,
    "stat" : CMD_STATUS,
    "s" : CMD_STATUS,
    "vstatus" : CMD_VSTATUS,
    "vstat" : CMD_VSTATUS,
    "vs" : CMD_VSTATUS,
    "clone" : CMD_CLONE,
    "co" : CMD_CLONE,
    "commit" : CMD_COMMIT,
    "com" : CMD_COMMIT,
    "ci" : CMD_COMMIT,
    "add" : CMD_ADD,
    "a" : CMD_ADD,
    "remove" : CMD_REMOVE,
    "rem" : CMD_REMOVE,
    "del" : CMD_REMOVE,
    "log" : CMD_LOG,
    "revert" : CMD_REVERT,
    "update" : CMD_UPDATE,
    "up" : CMD_UPDATE,
    "pull" : CMD_PULL,
    "push" : CMD_PUSH,
    "sync" : CMD_SYNC,
    "pending" : CMD_PEND,
    "pend" : CMD_PEND,
    "url" : CMD_URL,
    "loc" : CMD_URL,
    "backup" : CMD_BACKUP,
    "bkup" : CMD_BACKUP,
    "bkp" : CMD_BACKUP,
    "restore" : CMD_RESTORE,
    "rest" : CMD_RESTORE,
    "diff" : CMD_DIFF,
    "top" : CMD_TOP,
    "list" : CMD_LIST,
    "ls" : CMD_LIST,
    "find" : CMD_FIND,
    "project" : CMD_PROJ,
    "proj" : CMD_PROJ,
    "help" : CMD_HELP,
    }

### Config defaults
CFG_SECTIONS = ["global", "cvs", "git", "hg", "svn", "projects"]
CFG_DEFS = {
    "global" : {"topdir":"false"},
    "svn" : {},
    "hg" : {"status-topdir":"true", "revert-clean":"false"},
    "git" : {},
    "cvs" : {"filter-status":"true"},
    "projects" : {},
    }
# The global config object
cfg = None

### Help information
CMD_GROUPS = []
for i in range(0, len(CMD_NAMES)):
    CMD_GROUPS.append([])
for key,val in CMDS.iteritems():
    CMD_GROUPS[val].append(key)
# Sort by size, the hard way just cause
for arr in CMD_GROUPS:
    for idx in range(1, len(arr)):
        l = len(arr[idx])
        for ridx in range(idx - 1, -1, -1):
            rl = len(arr[ridx])
            if (l < rl):
                val = arr[idx]
                del arr[idx]
                arr[ridx + 1 : ridx + 1] = [val]
                break
            elif (ridx == 0) and (l > rl):
                val = arr[idx]
                del arr[idx]
                arr[ridx:ridx] = [val]
                break
    arr.append(CMDS[arr[0]])
CMD_GROUPS.sort()
CMD_HELP_TXT = \
{
    CMD_STATUS : "Show the status of the local checkout.",
    CMD_VSTATUS : "Use the underlying VCS command for status without filtering "
                  "the output.",
    CMD_CLONE : "Clone or checkout a repository.",
    CMD_COMMIT : "Commit outstanding changes.",
    CMD_ADD : "Add a file to the VCS.",
    CMD_REMOVE : "Remove a file from the VCS.",
    CMD_LOG : "Show the VCS version log.",
    CMD_REVERT : "Revert unchecked in changes.",
    CMD_UPDATE : "Update the local checkout against the repository.",
    CMD_PULL : "Pull changes from a remote repository.",
    CMD_PUSH : "Push local changes to a remote repository.",
    CMD_SYNC : "Synchronize local checkout against a remote repository.",
    CMD_PEND : "Show pending updates.",
    CMD_URL : "Show the URL of the remote repository.",
    CMD_BACKUP : "Backup unchecked in changes.",
    CMD_RESTORE : "Restore backed up changes.",
    CMD_DIFF : "Show the diff of unchecked in changes.",
    CMD_TOP : "Show the top directory of this checkout.",
    CMD_LIST : "List contents of repository URL.",
    CMD_FIND : "Run find and skip VCS files.",
    CMD_PROJ : "Run any VC command against all registered projects.",
    CMD_HELP : "Print this help message.",
}
VERSION = "0.0"
DESCRIPTION = \
    "VC is a common version control system frontend which provides a common interface for different VCS's. The default command is 'status'. To prevent interpretation of the command put a '--' before the command or options."


class VC(object):
    VCS = []

    def __init__(self, name, metadir, metacmd, cmds):
        self.name = name
        self.metadir = metadir
        self.metacmd = metacmd
        self.cmds = cmds
        self.topdir = None
        self.isfirst = True
        self.status_ignorables = []
        self.status_clean = False

    @staticmethod
    def detectByUrl(url):
        for vc in VC.VCS:
            if url[0] == vc.name:
                return url[1:], vc
        found = False
        for vc in VC.VCS:
            if vc.detectByUrl(url[0]):
                if found:
                    print "Ambiguous URL, please specify the VCS by name."
                    print "Use --help for help."
                    sys.exit(1)
                found = True
                ret = url, vc
        if found:
            return ret
        else:
            return url, None

    @staticmethod
    def _detectByUrl(url, urls):
        for prefix in urls:
            if re.search(prefix, url):
                return True
    
    @staticmethod
    def detectByPath():
        def recurseDetectVC(start, vc):
            last = None
            path = start.split(os.sep)
            for idx in range(0, len(path)):
                if idx == 0:
                    cur = os.sep.join(path)
                else:
                    cur = os.sep.join(path[:-idx])
                if vc.testPath(cur):
                    last = cur
                elif last:
                    return last
            return None
        start = os.getcwd()
        for vc in VC.VCS:
            top = None
            last = None
            path = start.split(os.sep)
            for idx in range(0, len(path)):
                if idx == 0:
                    cur = os.sep.join(path)
                else:
                    cur = os.sep.join(path[:-idx])
                if cur and len(cur) and vc.testPath(cur):
                    last = cur
                elif last:
                    top = last
                    break
            if top:
                vc.topdir = top
                return vc

    def testPath(self, start):
        return os.path.exists(os.path.join(start, self.metadir))

    def runCmd(self, cmd, argv, noexit=False, ignore_ret=False,
               capture_output=False, ask=ASK_NO):
        orgcmd = cmd
        if isinstance(cmd, types.StringType):
            cmd = "%s %s %s" % (self.metacmd, cmd, " ".join(argv))
        else:
            if isinstance(cmd, int):
                cmddef = self.cmds[cmd]
            else:
                cmddef = cmd
            if isinstance(cmddef, types.StringType):
                cmd = "%s %s %s" % (self.metacmd, cmddef, " ".join(argv))
            elif isinstance(cmddef, types.DictionaryType):
                if IGNORE in cmddef.keys():
                    ignore_ret = cmddef[IGNORE]
                if CMD in cmddef:
                    ret = self.runCmd(cmddef[CMD], argv, noexit=True,
                                      ignore_ret=ignore_ret,
                                      capture_output=capture_output,
                                      ask=ASK_ASK)
                    if isinstance(ret, types.TupleType):
                        check = ret[0]
                    else:
                        check = ret
                    if check != 0:
                        print "Command failed, exiting early."
                    return ret
                elif FULL in cmddef:
                    cmd = "%s %s" % (" ".join(cmddef[FULL]),
                                     " ". join(argv))
                elif MULTI in cmddef:
                    subask = ASK_ASK
                    for subcmd in cmddef[MULTI]:
                        ret = self.runCmd(subcmd, argv, noexit=True,
                                          ignore_ret=ignore_ret,
                                          capture_output=capture_output,
                                          ask=subask)
                        subask = ASK_ASK
                        if isinstance(ret, types.TupleType):
                            ret, subask = ret
                        if ret != 0:
                            print "Command failed, exiting early."
                            return ret
                    return ret
            elif callable(cmddef):
                rargs = {"noexit":noexit, "ignore_ret":ignore_ret,
                         "capture_output":capture_output, "ask":ask}
                return cmddef(argv, rargs)
            else:
                print "Invalid command type:", cmddef
                return -1
        cmd = self.filterCmd(cmd, orgcmd)
        if ask == ASK_ASK:
            if self.isfirst:
                self.isfirst = False
                print "Running:", cmd
            else:
                print "\nContinue: %s (Y/n) " % cmd, 
                ret = sys.stdin.readline()
                if ((not ret) or (len(ret) == 0) or (ret == "\n") or
                    ret.startswith("y") or ret.startswith("Y")):
                    pass
                else:
                    print "Exiting early."
                    sys.exit(-1)
        elif ((ask == ASK_SAY) or cfg.getBool(self, "verbose")):
            print "Running:", cmd
        if capture_output:
            ret = commands.getstatusoutput(cmd)
            if (isinstance(ignore_ret, types.ListType) and
                (ret[0] in ignore_ret)):
                ret[0] = 0
            if ((not noexit) or ((not ignore_ret) and (ret[0] != 0))):
                sys.exit(ret[0])
            else:
                return ret[1]
        else:
            ret = os.system(cmd)
            if (isinstance(ignore_ret, types.ListType) and
                (ret in ignore_ret)):
                ret = 0
            if ((not noexit) or ((not ignore_ret) and (ret != 0))):
                sys.exit(ret)
            else:
                return ret

    def filterCmd(self, cmd, orgcmd):
        return cmd
        
    def backupFile(self, path):
        tpath = path
        index = None
        while True:
            if index is None:
                tpath = path.replace("%s", "")
            else:
                tpath = path % ("-%d" % index)
            try:
                ret = os.stat(tpath)
            except OSError, e:
                if e.errno == 2:
                    return tpath.replace("%s", "")
                else:
                    raise e
            if index is None:
                index = 0
            else:
                index = index + 1
        return path.replace("%s", "")

    def listFiles(self, path):
        files = set()
        for dirpath, dirnames, filenames in os.walk(path):
            for name in filenames:
                files.add(os.path.join(dirpath, name))
        return files

    def cmdRestore(self, argv, rargs, depth=0):
        orgset = self.listFiles(self.topdir)
        cmd = "patch -p%d < %s" % (depth, argv[0])
        if cfg.getBool(self, "verbose"):
            print "Running:", cmd
        os.system(cmd)
        newset = self.listFiles(self.topdir)
        newfiles = newset - orgset
        if len(newfiles):
            self.runCmd(CMD_ADD, list(newfiles), **rargs)
        return 0

    def cmdTop(self, argv, rargs):
        print self.topdir
        return 0

    def cmdFind(self, argv, rargs):
        if len(argv):
            args = "'%s'" % "' '".join(argv)
        else:
            args = ""
        cmd = ("find %s | grep -v -e '.*/%s' -e '.*/%s/.*'" %
               (args, self.metadir, self.metadir))
        return os.system(cmd)

    def cmdSyncStatus(self, argv, rargs):
        subcmd = self.cmds[CMD_STATUS]
        rargs["capture_output"] = True
        ret = self.runCmd(subcmd, argv, **rargs)
        arr = []
        for line in ret.split("\n"):
            append = True
            for rx in self.status_ignorables:
                if rx.search(line):
                    append = False
                    break
            if append:
                arr.append(line)
        if len(ret) and len(arr):
            print ret
            return 0
        else:
            self.status_clean = True
            return (0, ASK_SAY)

    def cmdSyncCommit(self, argv, rargs):
        rargs["ask"] = ASK_ASK
        rargs["noexit"] = True
        if not self.status_clean:
            return self.runCmd(self.cmds[CMD_COMMIT], argv, **rargs)
        else:
            return (0, ASK_SAY)

class SVN(VC):
    URLS = ["^svn://", "^svn\\+ssh://", "^file://"]
    
    def __init__(self):
        VC.__init__(self, "svn", ".svn", "svn",
                    {CMD_STATUS : "status",
                     CMD_VSTATUS : "status",
                     CMD_CLONE : "checkout",
                     CMD_COMMIT : "commit",
                     CMD_ADD : "add",
                     CMD_REMOVE : "remove",
                     CMD_LOG : "log",
                     CMD_REVERT : self.cmdRevert,
                     CMD_UPDATE : "update",
                     CMD_PULL : "update",
                     CMD_PUSH : "commit",
                     CMD_SYNC : {MULTI : [self.cmdSyncStatus, CMD_UPDATE,
                                          self.cmdSyncCommit]},
                     CMD_PEND : "status -u",
                     CMD_URL : {FULL : ("svnpath",)},
                     CMD_BACKUP : self.cmdBackup,
                     CMD_RESTORE : self.cmdRestore,
                     CMD_DIFF : "diff",
                     CMD_TOP : self.cmdTop,
                     CMD_LIST : "list",
                     CMD_FIND : self.cmdFind,})
        self.status_ignorables.append(re.compile(r"^\?.*"))

    @staticmethod
    def detectByUrl(url):
        return VC._detectByUrl(url, SVN.URLS)

    def cmdRevert(self, argv, rargs):
        if len(argv) == 0:
            argv.append(".")
        return self.runCmd("revert -R", argv, **rargs)

    def cmdBackup(self, argv, rargs):
        ret = self.runCmd("info", [], noexit=True, capture_output=True)
        rev = None
        for line in ret.split("\n"):
            if line.startswith("Revision: "):
                words = line.split(":")
                rev = words[1].strip()
                break
        path = "backup-r%s" % rev
        path = self.backupFile(path + "%s.diff")
        ret = self.runCmd("diff > %s" % path, [], noexit=True)
        print "Backed up to:", path
        return ret


class HG(VC):
    URLS = ["^hg://", "^file://", "^ssh://", "^http[s]?://"]
    RX_TIP = re.compile("^tip[ \t]+.*")
    RX_CSET = re.compile("^changeset:")

    def __init__(self):
        VC.__init__(self, "hg", ".hg", "hg",
                    {CMD_STATUS : self.cmdStatus,
                     CMD_VSTATUS : "status",
                     CMD_CLONE : self.cmdClone,
                     CMD_COMMIT : "commit",
                     CMD_ADD : "add",
                     CMD_REMOVE : "remove",
                     CMD_LOG : "log",
                     CMD_REVERT : self.cmdRevert,
                     CMD_UPDATE : self.cmdUpdate,
                     CMD_PULL : "pull",
                     CMD_PUSH : "push",
                     CMD_SYNC : {MULTI :
                                     [self.cmdSyncStatus,
                                      {CMD : self.cmdSyncCommit, IGNORE : [256]},
                                      CMD_PULL, CMD_UPDATE, CMD_PUSH]},
                     CMD_PEND: self.cmdPend,
                     CMD_URL : "paths",
                     CMD_BACKUP : self.cmdBackup,
                     CMD_RESTORE : self.cmdRestore,
                     CMD_DIFF : "diff",
                     CMD_TOP : self.cmdTop,
                     CMD_LIST : "LIST",
                     CMD_FIND : self.cmdFind})
        self.status_ignorables.append(re.compile(r"^\?.*"))

    @staticmethod
    def detectByUrl(url):
        return VC._detectByUrl(url, HG.URLS)

    def cmdStatus(self, argv, rargs):
        if cfg.getBool(self, "status-topdir"):
            return self.runCmd("status", argv, **rargs)
        else:
            return self.runCmd("status", argv + ["."], **rargs)

    def cmdClone(self, argv, rargs):
        argv[0] = re.sub(HG.URLS[0], "", argv[0])
        return self.runCmd("clone", argv, **rargs)

    def cmdUpdate(self, argv, rargs):
        ret = self.runCmd("heads", [], noexit=True, capture_output=True)
        found = False
        for line in ret.split("\n"):
            if HG.RX_CSET.match(line):
                if found:
                    return self.cmdMerge(argv, rargs)
                else:
                    found = True
        return self.runCmd("update", argv, **rargs)

    def cmdMerge(self, argv, rargs):
        ret = self.runCmd("merge", argv, **rargs)
        if ret == 0:
            return self.runCmd("ci", argv, **rargs)
        else:
            return ret
    
    def cmdRevert(self, argv, rargs):
        if (("-r" not in argv) and cfg.getBool(self, "revert-clean")):
            ret = self.runCmd("identify", ["-i"], noexit=True,
                              capture_output=True)
            ret = ret.strip().rstrip("+")
            return self.runCmd("update", ["-C", "-r", ret] + argv, **rargs)
        else:        
            return self.runCmd("revert", argv, **rargs)

    def cmdPend(self, argv, rargs):
        print "Remote pending changesets:"
        self.runCmd("incoming", argv, True, True)
        print "\nLocal pending changesets:"
        rev = self.runCmd("identify -n", [], noexit=True,
                          capture_output=True).strip()
        rev = int(rev.rstrip("+"))
        ret = self.runCmd("tags", [], noexit=True, capture_output=True)
        for line in ret.split("\n"):
            if self.RX_TIP.match(line):
                words = line.split(" ")
                tip = int(words[-1:][0].split(":")[0])
                break
        if tip >= rev + 1:
            return self.runCmd("log -r %d:tip" % (rev + 1), argv)
        return 0

    def cmdBackup(self, argv, rargs):
        ret = self.runCmd("identify -n", [], noexit=True, capture_output=True)
        path = self.backupFile("backup-r" + ret.strip().rstrip("+") + "%s.diff")
        ret = self.runCmd("diff > %s" % path, [], noexit=True)
        print "Backed up to:", path
        return ret

    def cmdRestore(self, argv, rargs):
        git = False
        for line in open(argv[0]):
            if line.startswith("diff") and "--git" in line:
                git = True
                break
        if git:
            return VC.cmdRestore(self, argv, rargs, depth=1)
        else:
            return VC.cmdRestore(self, argv, rargs)


class GIT(VC):
    URLS = ["^git://", ".*\\.git$", ".*\\.git/$", "^file://", "^ssh://",
            "^rsync://", "^http[s]?://", "^ftp[s]?://"]

    def __init__(self):
        VC.__init__(self, "git", ".git", "git",
                    {CMD_STATUS : "status --porcelain",
                     CMD_VSTATUS : "status",
                     CMD_CLONE : "clone",
                     CMD_COMMIT : "commit -a",
                     CMD_ADD : "add",
                     CMD_REMOVE : "rm",
                     CMD_LOG : "log",
                     CMD_REVERT : "reset --hard",
                     CMD_UPDATE : "pull",
                     CMD_PULL : "fetch",
                     CMD_PUSH : "push",
                     CMD_SYNC : {MULTI :
                                     [self.cmdSyncStatus,
                                      {CMD : self.cmdSyncCommit, IGNORE : [256]},
                                      CMD_PULL, CMD_UPDATE, CMD_PUSH]},
                     CMD_PEND : "PENDING",
                     CMD_URL : "remote -v show",
                     CMD_BACKUP : self.cmdBackup,
                     CMD_RESTORE : self.cmdRestore,
                     CMD_DIFF : "diff",
                     CMD_TOP : self.cmdTop,
                     CMD_LIST : "LIST",
                     CMD_FIND : self.cmdFind,})
        self.status_ignorables.append(re.compile(r"^\?.*"))

    @staticmethod
    def detectByUrl(url):
        return VC._detectByUrl(url, GIT.URLS)

    def cmdBackup(self, argv, rargs):
        ret = self.runCmd("log --no-color --name-only --oneline -1", [],
                          noexit=True, capture_output=True)
        ret = ret.split("\n")[0].split(" ")[0]
        path = self.backupFile("backup-" + ret + "%s.diff")
        ret = self.runCmd("diff --no-color > %s" % path, [], noexit=True)
        print "Backed up to:", path
        return ret

    def cmdRestore(self, argv, rargs):
        return VC.cmdRestore(self, argv, rargs, depth=1)


class CVS(VC):
    URLS = ["^cvs://"]

    def __init__(self):
        VC.__init__(self, "cvs", "CVS", "cvs",
                    {CMD_STATUS : self.cmdStatus,
                     CMD_VSTATUS : "status",
                     CMD_CLONE : self.cmdClone,
                     CMD_COMMIT : "commit -R",
                     CMD_ADD : "add",
                     CMD_REMOVE : "remove",
                     CMD_LOG : "log",
                     CMD_REVERT : "update -RCd",
                     CMD_UPDATE : "update",
                     CMD_PULL : "update",
                     CMD_PUSH : "commit -R",
                     CMD_SYNC : {MULTI : [self.cmdSyncStatus, CMD_UPDATE,
                                          self.cmdSyncCommit]},
                     CMD_PEND : self.cmdPend,
                     CMD_URL : self.cmdUrl,
                     CMD_BACKUP : self.cmdBackup,
                     CMD_RESTORE : self.cmdRestore,
                     CMD_DIFF : "diff -u",
                     CMD_TOP : self.cmdTop,
                     CMD_LIST : self.cmdList,
                     CMD_FIND : self.cmdFind,})

    @staticmethod
    def detectByUrl(url):
        return VC._detectByUrl(url, CVS.URLS)

    def cmdClone(self, argv, rargs):
        url = re.sub(CVS.URLS[0], "", argv[0])
        nargv = [url] + ["rls"]
        if len(argv) > 1:
            nargv[1] = "checkout"
            nargv = nargv + argv[1:]
            return self.runCmd("-d", nargv, **rargs)
        else:
            url = url.split("/")
            l = len(url)
            for index in range(0, l):
                nargv[0] = "/".join(url[:(l - index)])
                if self.runCmd("-d", nargv + [">/dev/null", "2>&1"], noexit=True,
                               ignore_ret=True) == 0:
                    break
            nargv[1] = "checkout"
            nargv = nargv + ["/".join(url[(l - index):])]
            return self.runCmd("-d", nargv, **rargs)

    def cmdList(self, argv, rargs):
        url = re.sub(CVS.URLS[0], "", argv[0])
        nargv = [url] + ["rls"]
        if len(argv) > 1:
            nargv = nargv + argv[1:]
            return self.runCmd("-d", nargv, **rargs)
        else:
            url = url.split("/")
            l = len(url)
            for index in range(0, l):
                nargv[0] = "/".join(url[:(l - index)])
                if self.runCmd("-d", nargv + [">/dev/null", "2>&1"], noexit=True,
                               ignore_ret=True) == 0:
                    break
            nargv = nargv + ["/".join(url[(l - index):])]
            return self.runCmd("-d", nargv, **rargs)

    def cmdPend(self, argv, rargs):
        return self.runCmd("-n", ["update"] + argv, **rargs)

    def cmdUrl(self, argv, rargs):
        frepo = open(os.path.join(self.topdir, "CVS", "Repository"), "r")
        repo = ""
        for line in frepo:
            repo = line.strip()
            break
        frepo.close()
        froot = open(os.path.join(self.topdir, "CVS", "Root"), "r")
        root = ""
        for line in froot:
            root = line.strip()
            break
        froot.close()
        print "cvs://" + root + repo
        return 0
        
    def cmdBackup(self, argv, rargs):
        path = self.backupFile("backup%s.diff")
        ret = self.runCmd("diff -u > %s" % path, [], noexit=True,
                          ignore_ret=True)
        print "Backed up to:", path
        return ret

    def cmdStatus(self, argv, rargs):
        ret = self.runCmd("status", [], noexit=True, capture_output=True)
        if not cfg.getBool(self, "filter-status"):
            print ret
            return 0
        short = None
        status = None
        repo = None
        pwd = None
        new = False
        count = 0
        for line in ret.split("\n"):
            if line.startswith("=====") or new:
                if short:
                    show = self.mapStatus(status)
                    if show:
                        if pwd:
                            name = os.path.join(pwd, short)
                        else:
                            name = short
                        count = count + 1
                        print "%-8s%s" % (show, name)
                short = None
                status = None
                new = False
            elif line.startswith("File: "):
                idx = line.index("Status: ")
                short = line[6:idx-1].strip()
                status = line[idx+8:].strip()
            elif line.startswith("cvs status: Examining "):
                pwd = line.split()[-1:][0].strip()
                new = True
        if short:
            show = self.mapStatus(status)
            if show:
                if pwd:
                    name = os.path.join(pwd, short)
                else:
                    name = short
                count = count + 1
                print "%-8s%s" % (show, name)
        if count == 0:
            self.status_clean = True
        return 0

    def mapStatus(self, status):
        if status == "Locally Added":
            return "A"
        elif status == "Locally Modified":
            return "M"
        elif status == "Up-to-date":
            return None
        else:
            return status


VC.VCS.extend([SVN(), HG(), GIT(), CVS()])


class VcCfg(ConfigParser):
    def __init__(self, opts):
        self.opts = opts
        ConfigParser.__init__(self)

    def load(self):
        for secname in CFG_SECTIONS:
            section = CFG_DEFS[secname]
            self.add_section(secname)
            for key, val in section.iteritems():
                self.set(secname, key, val)
        self.read(os.path.expanduser(self.opts.config))

    def show(self):
        print "Config file:", self.opts.config
        for secname in CFG_SECTIONS:
            section = CFG_DEFS[secname]
            print "  [%s]" % secname
            for key, val in self.items(secname):
                print "   ", "%s:" % key, val

    def doGet(self, vc, key, getter):
        secname = "global"
        if vc:
            secname = vc.name
        try:
            val = getter(self, secname, key)
        except NoOptionError:
            try:
                val = getter(self, "global", key)
            except NoOptionError:
                if hasattr(self.opts, key):
                    val = getattr(self.opts, key)
                else:
                    val = None
        return val

    def getStr(self, vc, key):
        return self.doGet(vc, key, ConfigParser.get)
    
    def getInt(self, vc, key):
        return self.doGet(vc, key, ConfigParser.getint)

    def getBool(self, vc, key):
        return self.doGet(vc, key, ConfigParser.getboolean)

def runProjectCmd(argv):
    for name, path in cfg.items("projects"):
        cmd = ["vc"] + argv
        cmd = " ".join(cmd)
        print
        print "Running '%s' in %s..." % (cmd, name)
        fpath = os.path.expanduser(os.path.expandvars(path))
        if not os.path.exists(fpath):
            print "Invalid path:", path
            sys.exit(-1)
        else:
            os.chdir(fpath)
            os.system(cmd)

def doHelp(option, opt_str, value, parser):
    parser.print_help()
    print
    print "Commands:"
    for group in CMD_GROUPS:
        print "  ",
        start = True
        for cmd in group:
            if type(cmd) == types.StringType:
                if start:
                    sys.stdout.write("%s" % cmd)
                    start = False
                else:
                    sys.stdout.write(", %s" % cmd)
            else:
                print " -- ",
                print CMD_HELP_TXT[cmd]
    sys.exit(1)

def buildParser():
    parser = OptionParser(version=VERSION, description=DESCRIPTION,
                          usage="%prog [options] [cmd] [vcs options]")
    parser.disable_interspersed_args()
    parser.remove_option("-h")
    parser.add_option("-h", "--help", action="callback", callback=doHelp)
    parser.add_option("-c", "--config", type="string", default="~/.vcrc",
                      help="Configuration file to read.")
    parser.add_option("-p", "--print-cfg", action="store_true", default=False,
                      help="Print the loaded configuration.")
    parser.add_option("-v", "--verbose", action="store_true", default=False,
                      help="Display commands as they are run.")
    return parser

def main():
    global parser, cfg
    
    lookup = True
    found = False
    for arg in sys.argv:
        if not arg.startswith("-"):
            found = True
        elif arg == "--":
            if found:
                lookup = False
            break

    parser = buildParser()
    (opts, args) = parser.parse_args()
    cfg = VcCfg(opts)
    cfg.load()
    if opts.print_cfg:
        cfg.show()
        sys.exit(0)
    
    if len(args) == 1 and arg[0] == "--":
        cmd = CMD_VSTATUS
    elif len(args) > 0:
        cmd = args[0]
        try:
            if lookup:
                cmd = CMDS[cmd]
        except KeyError:
            pass
    else:
        cmd = CMD_STATUS

    if cmd == CMD_HELP:
        doHelp(None, None, None, parser)
    elif cmd == CMD_PROJ:
        ret = runProjectCmd(args[1:])
        sys.exit(ret)
    elif cmd in URL_CMDS:
        if len(args) > 1:
            nargs, vc = VC.detectByUrl(args[1:])
            args = args[0:1] + nargs
        else:
            doHelp(None, None, None, parser)
    else:
        vc = VC.detectByPath()
    if not vc:
        print "Unable to detect version control system."
        print "Use --help for help."
        sys.exit(1)
    if cfg.getBool(vc, "topdir") and (cmd == CMD_STATUS) and vc.topdir:
        os.chdir(vc.topdir)
    vc.runCmd(cmd, args[1:])

if __name__ == '__main__':
    try:
        main()
    except KeyboardInterrupt:
        pass
